﻿// -----------------------------------------------------------------------
// <copyright file="SubscriptionService.cs" company="Zen Design Corp">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace Zen.BlackBerry.Subscription
{
	using System;
	using System.Collections.Concurrent;
	using System.Collections.Concurrent.Partitioners;
	using System.Collections.Generic;
	using System.IO;
	using System.Linq;
	using System.Net;
	using System.Text;
	using System.Threading;
	using System.Threading.Tasks;
	using Microsoft.Practices.Unity;
	using Zen.BlackBerry.Common;
	using Zen.BlackBerry.Configuration;
	using Zen.BlackBerry.Pap;
	using Zen.BlackBerry.Pap.Control;
	using Zen.BlackBerry.Pap.Parser;
	using Zen.BlackBerry.Push;
	using Zen.BlackBerry.Push.AppManagement;
	using Zen.BlackBerry.Subscription.Count;
	using Zen.BlackBerry.Subscription.Sync;
	using Zen.BlackBerry.Subscription.Web;

	/// <summary>
	/// Implementation of the <see cref="ISubscriptionService"/> interface.
	/// </summary>
	public class SubscriptionService : ISubscriptionService
	{
		private class SwapBISUnsubscribeRequest
		{
			public string PushApplicationId
			{
				get;
				set;
			}

			public string PushApplicationPassword
			{
				get;
				set;
			}

			public string SubscriberAddress
			{
				get;
				set;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="SubscriptionService"/> class.
		/// </summary>
		public SubscriptionService()
		{
		}

		/// <summary>
		/// Gets or sets the repository.
		/// </summary>
		/// <value>The repository.</value>
		[Dependency]
		public ISubscriptionRepository Repository
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the push properties.
		/// </summary>
		/// <value>The push properties.</value>
		[Dependency]
		public IPushProperties PushProperties
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the pap service.
		/// </summary>
		/// <value>The pap service.</value>
		[Dependency]
		public IPapService PapService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the push service.
		/// </summary>
		/// <value>The push service.</value>
		[Dependency]
		public Func<IPushService> PushService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the application service.
		/// </summary>
		/// <value>The application service.</value>
		[Dependency]
		public Func<IPushApplicationService> ApplicationService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the content provider subscription service.
		/// </summary>
		/// <value>The content provider subscription service.</value>
		[Dependency]
		public IContentProviderSubscriptionService ContentProviderSubscriptionService
		{
			get;
			set;
		}

		/// <summary>
		/// Validates the subscribe fields.
		/// </summary>
		/// <param name="subRequest">The sub request.</param>
		protected virtual void ValidateSubscribeFields(SubscribeRequest subRequest)
		{
		}

		/// <summary>
		/// Validates the subscriber unique key.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberId">The subscriber id.</param>
		protected virtual void ValidateSubscriberUniqueKey(string pushApplicationId, string subscriberId)
		{
			if (string.IsNullOrWhiteSpace(pushApplicationId))
			{
				throw new ArgumentException("Invalid push application id specified.");
			}

			if (string.IsNullOrWhiteSpace(subscriberId))
			{
				throw new ArgumentException("Invalid subscriber id specified.");
			}
		}

		/// <summary>
		/// Validates the subscriber exists.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <returns></returns>
		protected Subscriber ValidateSubscriberExists(string pushApplicationId, string subscriberId)
		{
			Subscriber sub = Repository.FindByAppIdAndId(pushApplicationId, subscriberId);
			if (sub == null)
			{
				throw new ArgumentException("Subscriber does not exist (appId=" + pushApplicationId + ", sub=" + subscriberId + ").");
			}
			return sub;
		}

		private async Task SyncSubscribersWithPPGForStatusAsync(
			SubscriberSyncResult result,
			PushApplication app,
			SubscriberStatus status,
			int startIndex,
			int endIndex,
			SemaphoreSlim maxThreads = null)
		{
			if (maxThreads != null)
			{
				maxThreads.Wait();
			}
			try
			{
				// Get the list of subscribers
				IEnumerable<SubscriberPartial> subscribers =
					Repository.FindByAppIdAndTypeAndStatus(
						app.RimApplicationId,
						SubscriberType.Public,
						status,
						startIndex,
						endIndex);

				// Get the status of this subscriber from the PPG
				SubscriptionQueryControl control =
					new SubscriptionQueryControl
					{
						IsUsingPublicPush = true,
						PushProperties = PushProperties,
						Addresses = subscribers.Select((item) => item.Address).ToArray()
					};

				// Get the current status
				SubscriptionQueryResponse response =
					await PapService.SubscriptionQuery(
						app.UserName, app.PublicPassword, control);
				foreach (var subResult in response.Results)
				{
					// Find the associated subscriber
					SubscriberPartial sub = subscribers.First((entity) => entity.Address == subResult.Address);

					// Compare status codes
					if (subResult.Status == SubscriptionStatus.Active)
					{
						if (sub.Status == SubscriberStatus.Active)
						{
							continue;
						}
						else if (sub.Status == SubscriberStatus.Inactive)
						{
							try
							{
								await Unsubscribe(
									new UnsubscribeRequest(app.RimApplicationId, sub.SubscriberId)
									{
										IsSystemGenerated = true,
									});
							}
							catch
							{
								result.SdkSubscriberUpdateFailures.Add(
									new SDKSubscriberUpdateFailure(
										SubscriptionType.Unsubscribe,
										app.RimApplicationId,
										sub.SubscriberId));
							}
						}
						else if (sub.Status == SubscriberStatus.Suspended)
						{
							try
							{
								await SuspendSubscription(
									new SuspendRequest(
										app.RimApplicationId,
										sub.SubscriberId));
							}
							catch
							{
								result.SdkSubscriberUpdateFailures.Add(
									new SDKSubscriberUpdateFailure(
										SubscriptionType.Suspend,
										app.RimApplicationId,
										sub.SubscriberId));
							}
						}
					}
					else if (subResult.Status == SubscriptionStatus.Suspended)
					{
						if (sub.Status == SubscriberStatus.Suspended)
						{
							continue;
						}
						else if (sub.Status == SubscriberStatus.Inactive)
						{
							try
							{
								await Unsubscribe(
									new UnsubscribeRequest(
										app.RimApplicationId,
										sub.SubscriberId)
									{
										IsSystemGenerated = true,
									});
							}
							catch
							{
								result.SdkSubscriberUpdateFailures.Add(
									new SDKSubscriberUpdateFailure(
										SubscriptionType.Unsubscribe,
										app.RimApplicationId,
										sub.SubscriberId));
							}
						}
						else if (sub.Status == SubscriberStatus.Active)
						{
							try
							{
								await NotifyPPG(SubscriptionType.Suspend, app.RimApplicationId, sub.Address);
							}
							catch
							{
								result.SdkSubscriberUpdateFailures.Add(
									new SDKSubscriberUpdateFailure(
										SubscriptionType.Suspend,
										app.RimApplicationId,
										sub.SubscriberId));
							}
						}
					}
					else if (subResult.Status == SubscriptionStatus.Unsubscribed)
					{
						if (sub.Status == SubscriberStatus.Inactive)
						{
							continue;
						}
						else
						{
							try
							{
								await NotifyPPG(SubscriptionType.Suspend, app.RimApplicationId, sub.Address);
							}
							catch
							{
								result.SdkSubscriberUpdateFailures.Add(
									new SDKSubscriberUpdateFailure(
										SubscriptionType.Unsubscribe,
										app.RimApplicationId,
										sub.SubscriberId));
							}
						}
					}
				}
			}
			finally
			{
				if (maxThreads != null)
				{
					maxThreads.Release();
				}
			}
		}

		private void ValidateSmallNumberOfSubscribers(string applicationId, List<string> subscriberIds, SubscriptionValidationResult subscriptionValidationResult)
		{
			foreach (string subscriberId in subscriberIds)
			{
				SubscriberPartial subscriber = Repository.FindByAppIdAndIdPartial(applicationId, subscriberId);

				if (subscriber == null)
				{
					subscriptionValidationResult.DoNotExistSubscriberIds.Add(subscriberId);
				}
				else
				{
					switch (subscriber.Status)
					{
						case SubscriberStatus.Inactive:
							subscriptionValidationResult.InactiveSubscriberIds.Add(subscriberId);
							break;

						case SubscriberStatus.Suspended:
							subscriptionValidationResult.SuspendedSubscriberIds.Add(subscriberId);
							break;

						case SubscriberStatus.Active:
							subscriptionValidationResult.ActiveSubscribers.Add(subscriber);
							subscriptionValidationResult.ActiveAddresses.Add(subscriber.Address);
							break;
					}
				}
			}
		}

		private bool ValidateSubscribe(SubscribeRequest subRequest, List<SwapBISUnsubscribeRequest> swapUnsubscribeList, List<UnsubscribeRequest> swapNotificationList)
		{
			//if (log.isDebugEnabled())
			//{
			//    log.debug("Subscribe Data passed in: " + subRequest);
			//}

			ValidateSubscribeFields(subRequest);
			PushApplication pushApp = ApplicationService().FindByIdAndValidate(subRequest.PushApplicationId);

			if (pushApp.BypassSubscriptions)
			{
				//log.warn("Attempt to subscribe for a push application that bypasses subscription: pushAppId=" + subRequest.getPushApplicationId() + ", subscriberId=" + subRequest.getSubscriberId() + ", address=" + subRequest.getAddress());
				throw new NotSupportedException("Subscription not allowed for push application " + subRequest.PushApplicationId + ".");
			}

			if (subRequest.Type == null)
			{
				if (pushApp.Type == PushApplicationType.PublicPush)
				{
					subRequest.Type = SubscriberType.Public;
				}
				else if (pushApp.Type == PushApplicationType.EnterprisePush)
				{
					subRequest.Type = SubscriberType.Enterprise;
				}
				else
				{
					//log.warn("The subscriber type was invalid (null, empty, or not one of " + SubscriberType.PUBLIC + " or " + SubscriberType.ENTERPRISE + ") for applications of type " + PushApplicationType.PUBLIC_AND_ENTERPRISE_PUSH + ".");
					throw new ArgumentException("Invalid type specified.");
				}
			}

			if ((pushApp.Type == PushApplicationType.PublicPush) && (subRequest.Type == SubscriberType.Enterprise))
			{
				//log.warn("Subscriber type was " + subRequest.getType() + " which is not allowed for a push application with a type of " + pushApp.getType() + ".");
				throw new ArgumentException("Invalid type specified.");
			}
			if ((pushApp.Type == PushApplicationType.EnterprisePush) && (subRequest.Type == SubscriberType.Public))
			{
				//log.warn("Subscriber type was " + subRequest.getType() + " which is not allowed for a push application with a type of " + pushApp.getType() + ".");
				throw new ArgumentException("Invalid type specified.");
			}

			Subscriber subStorage = Repository.FindByAppIdAndId(subRequest.PushApplicationId, subRequest.SubscriberId);
			//if (log.isDebugEnabled())
			//{
			//    log.debug("Subscriber Data from storage: " + subStorage);
			//}

			if (subStorage != null)
			{
				if (subStorage.Status == SubscriberStatus.Inactive)
				{
					var subList = Repository.FindByIdAndNotStatus(subRequest.SubscriberId, SubscriberStatus.Inactive);

					if (!subList.Any())
					{
						SubscriberSwap(subRequest, swapUnsubscribeList, swapNotificationList);
					}
					else
					{
						String oldAddress = GetOldAddress(subList);
						if (oldAddress != subRequest.Address)
						{
							DeviceSwap(oldAddress, subRequest.SubscriberId, subRequest.PushApplicationId, swapUnsubscribeList, swapNotificationList);
							SubscriberSwap(subRequest, swapUnsubscribeList, swapNotificationList);
						}
					}
				}
				else if (subStorage.Status == SubscriberStatus.Active)
				{
					if (subStorage.Address == subRequest.Address)
					{
						if (pushApp.Type == PushApplicationType.PublicEnterprisePush)
						{
							UnsubscribeOnTypeChange(subStorage.Type, subRequest.Type.Value, pushApp.RimApplicationId, pushApp.PublicPassword, subRequest.Address, swapUnsubscribeList);
						}
						return true;
					}

					DeviceSwap(subStorage.Address, subStorage.SubscriberId, subStorage.PushApplicationId, swapUnsubscribeList, swapNotificationList);
					SubscriberSwap(subRequest, swapUnsubscribeList, swapNotificationList);
				}
				else if (subStorage.Status == SubscriberStatus.Suspended)
				{
					if (!subStorage.Address.Equals(subRequest.Address, StringComparison.OrdinalIgnoreCase))
					{
						DeviceSwap(subStorage.Address, subStorage.SubscriberId, subStorage.PushApplicationId, swapUnsubscribeList, swapNotificationList);
						SubscriberSwap(subRequest, swapUnsubscribeList, swapNotificationList);
					}
					else if (pushApp.Type == PushApplicationType.PublicEnterprisePush)
					{
						UnsubscribeOnTypeChange(subStorage.Type, subRequest.Type.Value, pushApp.RimApplicationId, pushApp.PublicPassword, subRequest.Address, swapUnsubscribeList);
					}
				}
				return true;
			}
			return false;
		}

		private void UnsubscribeOnTypeChange(SubscriberType currentType, SubscriberType newType, string appId, string publicPassword, string address, List<SwapBISUnsubscribeRequest> swapUnsubscribeList)
		{
			if ((currentType == SubscriberType.Public) && (newType == SubscriberType.Enterprise))
			{
				SwapBISUnsubscribeRequest unsubscribeRequest =
					new SwapBISUnsubscribeRequest
					{
						PushApplicationId = appId,
						PushApplicationPassword = publicPassword,
						SubscriberAddress = address
					};
				swapUnsubscribeList.Add(unsubscribeRequest);
			}
		}

		private Subscriber ValidateUnsubscribe(UnsubscribeRequest unsubRequest)
		{
			//if (log.isDebugEnabled())
			//{
			//    log.debug("Unsubscribe Data passed in: " + unsubRequest);
			//}

			return ValidateUnsubscribeOrSuspend(unsubRequest.PushApplicationId, unsubRequest.SubscriberId);
		}

		private Subscriber ValidateSuspendSubscription(SuspendRequest suspRequest)
		{
			//if (log.isDebugEnabled())
			//{
			//    log.debug("Suspend Data passed in: " + suspRequest);
			//}

			Subscriber subFromStorage = ValidateUnsubscribeOrSuspend(suspRequest.PushApplicationId, suspRequest.SubscriberId);
			ValidateSubscriberStatus(suspRequest.PushApplicationId, suspRequest.SubscriberId, subFromStorage.Status, SubscriptionType.Suspend);
			return subFromStorage;
		}

		private Subscriber ValidateResumeSubscription(ResumeRequest resumeRequest)
		{
			//if (log.isDebugEnabled())
			//{
			//    log.debug("Resume Data passed in: " + resumeRequest);
			//}

			ValidateSubscriberUniqueKey(resumeRequest.PushApplicationId, resumeRequest.SubscriberId);

			PushApplication pushApp = ApplicationService().FindByIdAndValidate(resumeRequest.PushApplicationId);

			if (pushApp.BypassSubscriptions)
			{
				//log.warn("Attempt to resume for a push application that bypasses subscription: pushAppId=" + resumeRequest.getPushApplicationId() + ", subscriberId=" + resumeRequest.getSubscriberId());
				throw new NotSupportedException("Subscription operations not allowed for push application " + resumeRequest.PushApplicationId + ".");
			}

			Subscriber subFromStorage = ValidateSubscriberExists(resumeRequest.PushApplicationId, resumeRequest.SubscriberId);
			ValidateSubscriberStatus(resumeRequest.PushApplicationId, resumeRequest.SubscriberId, subFromStorage.Status, SubscriptionType.Resume);
			return subFromStorage;
		}

		private Subscriber ValidateUnsubscribeOrSuspend(string pushApplicationId, string subscriberId)
		{
			ValidateSubscriberUniqueKey(pushApplicationId, subscriberId);
			ValidatePushApplication(pushApplicationId);
			return ValidateSubscriberExists(pushApplicationId, subscriberId);
		}

		private PushApplication ValidatePushApplication(string pushApplicationId)
		{
			PushApplication pushApplication = ApplicationService().FindByAppId(pushApplicationId);
			if (pushApplication == null)
			{
				//log.warn("Push Application could not be found: pushAppId=" + pushApplicationId);
				throw new ArgumentException("Application does not exist.");
			}

			if (pushApplication.BypassSubscriptions)
			{
				//log.warn("Subscription operation attempted for a push application that bypasses subscription: pushAppId=" + pushApplicationId);
				throw new NotSupportedException("Subscription operation attempted for a push application that bypasses subscription: pushAppId=" + pushApplicationId);
			}
			return pushApplication;
		}

		private void ValidateSubscriberStatus(string pushApplicationId, string subscriberId, SubscriberStatus currentStatus, SubscriptionType subscriptionType)
		{
			if ((currentStatus != SubscriberStatus.Inactive) || ((subscriptionType != SubscriptionType.Resume) && (subscriptionType != SubscriptionType.Suspend)))
			{
				return;
			}
			//log.warn("Invalid status for subscriber (appId=" + pushApplicationId + ", subId=" + subscriberId + "): currentStatus=" + currentStatus + ", subscriptionType=" + subscriptionType);
			throw new ArgumentException("Invalid subscriber status."); //SubscriberStatus.INACTIVE, subscriptionType);
		}

		private void ValidateDeleteInactiveSubscription(string pushApplicationId, string subscriberId, string deletedBy)
		{
			if (string.IsNullOrEmpty(pushApplicationId))
			{
				throw new ArgumentNullException("pushApplicationId");
			}
			if (string.IsNullOrEmpty(subscriberId))
			{
				throw new ArgumentNullException("subscriberId");
			}
			if (string.IsNullOrEmpty(deletedBy))
			{
				throw new ArgumentNullException("deletedBy");
			}

			PushApplication pushApplication = ApplicationService().FindByAppId(pushApplicationId);
			if (pushApplication == null)
			{
				//log.warn("Push Application could not be found: pushAppId=" + pushApplicationId);
				throw new ArgumentException("Push application not found.");
			}

			Subscriber subscriber = ValidateSubscriberExists(pushApplicationId, subscriberId);
			if (subscriber.Status != SubscriberStatus.Inactive)
			{
				throw new InvalidOperationException("Can only delete INACTIVE subscribers. Current status of subscriber: " + subscriber.Status + " (pushAppId=" + pushApplicationId + ", subscriberId=" + subscriberId + ")");
			}
		}

		private void ValidateDeleteSubscriptions(string pushApplicationId, string deletedBy)
		{
			if (string.IsNullOrEmpty(pushApplicationId))
			{
				throw new ArgumentNullException("pushApplicationId");
			}
			if (string.IsNullOrEmpty(deletedBy))
			{
				throw new ArgumentNullException("deletedBy");
			}

			PushApplication pushApplication = ApplicationService().FindByAppId(pushApplicationId);
			if (pushApplication == null)
			{
				//log.warn("Push Application could not be found: pushAppId=" + pushApplicationId);
				throw new ArgumentException("Push application not found.");
			}
		}

		private void SubscriberSwap(SubscribeRequest subRequest, List<SwapBISUnsubscribeRequest> swapUnsubscribeList, List<UnsubscribeRequest> swapNotificationList)
		{
			var subList = Repository.FindByAddressAndNotIdAndNotStatus(subRequest.Address, subRequest.SubscriberId, SubscriberStatus.Inactive);
			foreach (SubscriberPartial subPartial in subList)
			{
				UnsubscribeRequest unsubRequest = new UnsubscribeRequest(
					subPartial.PushApplicationId, subPartial.SubscriberId);
				Repository.Unsubscribe(unsubRequest);
				swapNotificationList.Add(unsubRequest);

				if ((subPartial.Type == SubscriberType.Public) &&
					(subPartial.PushApplicationId != subRequest.PushApplicationId))
				{
					PushApplication app = ApplicationService().FindByAppId(subPartial.PushApplicationId);
					SwapBISUnsubscribeRequest unsubscribeRequest =
						new SwapBISUnsubscribeRequest
						{
							PushApplicationId = app.RimApplicationId,
							PushApplicationPassword = app.PublicPassword,
							SubscriberAddress = subPartial.Address
						};
					swapUnsubscribeList.Add(unsubscribeRequest);
				}
			}
		}

		private void DeviceSwap(string oldAddress, string subscriberId, string pushAppId, List<SwapBISUnsubscribeRequest> swapUnsubscribeList, List<UnsubscribeRequest> swapNotificationList)
		{
			var subList = Repository.FindByAddressAndIdAndNotStatus(oldAddress, subscriberId, SubscriberStatus.Inactive);
			foreach (SubscriberPartial subPartial in subList)
			{
				if (subPartial.PushApplicationId != pushAppId)
				{
					UnsubscribeRequest unsubRequest = new UnsubscribeRequest(
						subPartial.PushApplicationId, subPartial.SubscriberId);
					Repository.Unsubscribe(unsubRequest);
					swapNotificationList.Add(unsubRequest);
				}

				if (subPartial.Type == SubscriberType.Public)
				{
					PushApplication app = ApplicationService().FindByAppId(subPartial.PushApplicationId);
					SwapBISUnsubscribeRequest unsubscribeRequest =
						new SwapBISUnsubscribeRequest
						{
							PushApplicationId = app.RimApplicationId,
							PushApplicationPassword = app.PublicPassword,
							SubscriberAddress = subPartial.Address
						};
					swapUnsubscribeList.Add(unsubscribeRequest);
				}
			}
		}

		private async Task NotifyPPGCore(string ppgUriPrefix, string pushApplicationId, string publicPassword, string address)
		{
			Uri ppgUri = new Uri(ppgUriPrefix + address);
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ppgUri);
			request.Timeout = PushProperties.HttpConnectionTimeout;
			request.ReadWriteTimeout = PushProperties.HttpReadTimeout;
			request.Method = "GET";
			string rawCredentials = string.Format("{0}:{1}", pushApplicationId, publicPassword);
			request.Headers.Add("Authorization",
				string.Format(
					"Basic {0}",
					Convert.ToBase64String(Encoding.UTF8.GetBytes(rawCredentials))));

			HttpWebResponse response = (HttpWebResponse)
				await request.GetResponseAsync();
			string resultCode = string.Empty;
			if (response.ContentLength > 0)
			{
				using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
				{
					resultCode = reader.ReadToEnd();
				}
				if (resultCode.StartsWith("rc="))
				{
					resultCode = resultCode.Substring(3);
				}
			}

			if (response.StatusCode == HttpStatusCode.OK)
			{
				SubscriptionStatusCode code =
					XmlExtensions.GetEnumFromText<SubscriptionStatusCode>(
					resultCode, SubscriptionStatusCode.SystemError);

				// TODO: Check codes and throw PPG comms error
			}
		}

		private string GetOldAddress(IEnumerable<SubscriberPartial> subList)
		{
			string oldAddress = null;
			foreach (var subPartial in subList)
			{
				if (oldAddress == null)
				{
					oldAddress = subPartial.Address;
				}
				else if (subPartial.Address != oldAddress)
				{
					throw new InvalidOperationException("The addresses from all the subscribers in the list should match.");
				}
			}
			return oldAddress;
		}

		#region ISubscriptionService Members

		/// <summary>
		/// Deletes an inactive subscription from storage.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <param name="deletedBy">The deleted by.</param>
		public void DeleteInactiveSubscription(string pushApplicationId, string subscriberId, string deletedBy)
		{
			ValidateDeleteInactiveSubscription(pushApplicationId, subscriberId, deletedBy);
			Repository.DeleteInactiveSubscription(pushApplicationId, subscriberId);

			// TODO: Write to log
		}

		/// <summary>
		/// Deletes all the subscriptions from storage (and potentially the cache) for a push application entirely and permanently.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="deletedBy">The deleted by.</param>
		public void DeleteSubscriptions(string pushApplicationId, string deletedBy)
		{
			ValidateDeleteSubscriptions(pushApplicationId, deletedBy);
			Repository.DeleteSubscriptions(pushApplicationId);

			// TODO: Write to log
		}

		/// <summary>
		/// Finds a list of subscribers with the specified address and the specified subscriber id and NOT the specified status.
		/// </summary>
		/// <param name="address">The address.</param>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAddressAndIdAndNotStatus(string address, string subscriberId, SubscriberStatus status)
		{
			return Repository.FindByAddressAndIdAndNotStatus(address, subscriberId, status);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified address and NOT the specified subscriber id and NOT the specified status.
		/// </summary>
		/// <param name="address">The address.</param>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAddressAndNotIdAndNotStatus(string address, string subscriberId, SubscriberStatus status)
		{
			return Repository.FindByAddressAndNotIdAndNotStatus(address, subscriberId, status);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified push application id.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="startIndex">The start index.</param>
		/// <param name="endIndex">The end index.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAppId(string pushApplicationId, int startIndex, int endIndex)
		{
			return Repository.FindByAppId(pushApplicationId, startIndex, endIndex);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified push application id and address.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="address">The address.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAppIdAndAddress(string pushApplicationId, string address)
		{
			return Repository.FindByAppIdAndAddress(pushApplicationId, address);
		}

		/// <summary>
		/// Finds the subscriber with the specified id and push application id.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <returns>A <see cref="Subscriber"/> object.</returns>
		public Subscriber FindByAppIdAndId(string pushApplicationId, string subscriberId)
		{
			return Repository.FindByAppIdAndId(pushApplicationId, subscriberId);
		}

		/// <summary>
		/// Finds a list of subscribers for the given push application id that are in the specified subscriber id list with the specified status.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberIds">The subscriber ids.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAppIdAndIdsAndStatus(string pushApplicationId, IEnumerable<string> subscriberIds, SubscriberStatus status)
		{
			int subscribers = subscriberIds.Count();
			if (subscribers > PushProperties.MaxInClauseValues)
			{
				// Setup partitioner
				OrderablePartitioner<string> part = Partitioner.Create(subscriberIds);
				int partitionCount = (subscribers / PushProperties.MaxInClauseValues) + 1;
				IList<IEnumerator<string>> partitions = part.GetPartitions(partitionCount);

				ConcurrentStack<SubscriberPartial> stack = new ConcurrentStack<SubscriberPartial>();
				Parallel.ForEach(
					partitions,
					new ParallelOptions
					{
						MaxDegreeOfParallelism = PushProperties.SubscriptionMatchingMaxThreads
					},
					(block) =>
					{
						stack.PushRange(Repository.FindByAppIdAndIdsAndStatus(
							pushApplicationId, new EnumerableWrapper<string>(block), status).ToArray());
					});
				return stack;
			}
			else
			{
				return Repository.FindByAppIdAndIdsAndStatus(pushApplicationId, subscriberIds, status);
			}
		}

		/// <summary>
		/// Finds a list of subscribers with the specified push application id and status.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="status">The status.</param>
		/// <param name="startIndex">The start index.</param>
		/// <param name="endIndex">The end index.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAppIdAndStatus(string pushApplicationId, SubscriberStatus status, int startIndex, int endIndex)
		{
			return Repository.FindByAppIdAndStatus(pushApplicationId, status, startIndex, endIndex);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified push application id and type.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="type">The type.</param>
		/// <param name="startIndex">The start index.</param>
		/// <param name="endIndex">The end index.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAppIdAndType(string pushApplicationId, SubscriberType type, int startIndex, int endIndex)
		{
			return Repository.FindByAppIdAndType(pushApplicationId, type, startIndex, endIndex);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified push application id, type, and status.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="type">The type.</param>
		/// <param name="status">The status.</param>
		/// <param name="startIndex">The start index.</param>
		/// <param name="endIndex">The end index.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByAppIdAndTypeAndStatus(string pushApplicationId, SubscriberType type, SubscriberStatus status, int startIndex, int endIndex)
		{
			return Repository.FindByAppIdAndTypeAndStatus(pushApplicationId, type, status, startIndex, endIndex);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified id.
		/// </summary>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindById(string subscriberId)
		{
			return Repository.FindById(subscriberId);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified id and NOT the specified status.
		/// </summary>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByIdAndNotStatus(string subscriberId, SubscriberStatus status)
		{
			return Repository.FindByIdAndNotStatus(subscriberId, status);
		}

		/// <summary>
		/// Finds a list of subscribers with the specified id and status.
		/// </summary>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByIdAndStatus(string subscriberId, SubscriberStatus status)
		{
			return Repository.FindByIdAndStatus(subscriberId, status);
		}

		/// <summary>
		/// Finds a list of subscribers whose id contains the given subscriberIdPattern.
		/// </summary>
		/// <param name="subscriberIdPattern">The subscriber id pattern.</param>
		/// <param name="startIndex">The start index.</param>
		/// <param name="endIndex">The end index.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> FindByIdPattern(string subscriberIdPattern, int startIndex, int endIndex)
		{
			return Repository.FindByIdPattern(subscriberIdPattern, startIndex, endIndex);
		}

		/// <summary>
		/// Gets a count of the number of users of a push application (regardless of status) for each BlackBerry device model.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <returns>
		/// An enumeration of <see cref="DeviceModelCount"/> objects.
		/// </returns>
		public IEnumerable<DeviceModelCount> GetDeviceModelCount(string pushApplicationId)
		{
			return Repository.GetDeviceModelCount(pushApplicationId);
		}

		/// <summary>
		/// Gets a count of the number of users of a push application with a given status for each BlackBerry device model.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="DeviceModelCount"/> objects.
		/// </returns>
		public IEnumerable<DeviceModelCount> GetDeviceModelCount(string pushApplicationId, SubscriberStatus status)
		{
			return Repository.GetDeviceModelCount(pushApplicationId, status);
		}

		/// <summary>
		/// Gets a count of the number of users of a push application (regardless of status) for each OS version running on a BlackBerry device.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <returns>
		/// An enumeration of <see cref="OsVersionCount"/> objects.
		/// </returns>
		public IEnumerable<OsVersionCount> GetOsVersionCount(string pushApplicationId)
		{
			return Repository.GetOsVersionCount(pushApplicationId);
		}

		/// <summary>
		/// Gets a count of the number of users of a push application with a given status for each OS version running on a BlackBerry device.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="OsVersionCount"/> objects.
		/// </returns>
		public IEnumerable<OsVersionCount> GetOsVersionCount(string pushApplicationId, SubscriberStatus status)
		{
			return Repository.GetOsVersionCount(pushApplicationId, status);
		}

		/// <summary>
		/// Gets a count of the number of users that had their subscriptions resumed for an application between the given date range.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="fromDate">From date.</param>
		/// <param name="toDate">To date.</param>
		/// <returns>
		/// A count of users who had their subscriptions resumed.
		/// </returns>
		public int GetResumeCount(string pushApplicationId, DateTime fromDate, DateTime toDate)
		{
			return Repository.GetResumeCount(pushApplicationId, fromDate, toDate);
		}

		/// <summary>
		/// Gets a count of all the subscribers in storage with the specified push application id.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <returns>A count of subscribers.</returns>
		public int GetSizeByAppId(string pushApplicationId)
		{
			return Repository.GetSizeByAppId(pushApplicationId);
		}

		/// <summary>
		/// Gets a count of all the subscribers in storage with the specified push application id and status.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="status">The status.</param>
		/// <returns>A count of subscribers.</returns>
		public int GetSizeByAppIdAndStatus(string pushApplicationId, SubscriberStatus status)
		{
			return Repository.GetSizeByAppIdAndStatus(pushApplicationId, status);
		}

		/// <summary>
		/// Gets a count of all the subscribers in storage with the specified push application id and type.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="type">The type.</param>
		/// <returns>A count of subscribers.</returns>
		public int GetSizeByAppIdAndType(string pushApplicationId, SubscriberType type)
		{
			return Repository.GetSizeByAppIdAndType(pushApplicationId, type);
		}

		/// <summary>
		/// Gets a count of all the subscribers in storage with the specified push application id, type, and status.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="type">The type.</param>
		/// <param name="status">The status.</param>
		/// <returns>A count of subscribers.</returns>
		public int GetSizeByAppIdAndTypeAndStatus(string pushApplicationId, SubscriberType type, SubscriberStatus status)
		{
			return Repository.GetSizeByAppIdAndTypeAndStatus(pushApplicationId, type, status);
		}

		/// <summary>
		/// Gets a count of all the subscribers in storage whose id contains the given subscriberIdPattern.
		/// </summary>
		/// <param name="subscriberIdPattern">The subscriber id pattern.</param>
		/// <returns>A count of subscribers.</returns>
		public int GetSizeByIdPattern(string subscriberIdPattern)
		{
			return Repository.GetSizeByIdPattern(subscriberIdPattern);
		}

		/// <summary>
		/// Gets a count of the number of new subscribers to an application for a given date range.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="fromDate">From date.</param>
		/// <param name="toDate">To date.</param>
		/// <returns>A count of new subscribers.</returns>
		public int GetSubscribeCount(string pushApplicationId, DateTime fromDate, DateTime toDate)
		{
			return Repository.GetSubscribeCount(pushApplicationId, fromDate, toDate);
		}

		/// <summary>
		/// Gets a count of the number of users that had their subscriptions suspended for an application between the given date range.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="fromDate">From date.</param>
		/// <param name="toDate">To date.</param>
		/// <returns>
		/// A count of users who had their subscription suspended.
		/// </returns>
		public int GetSuspendCount(string pushApplicationId, DateTime fromDate, DateTime toDate)
		{
			return Repository.GetSuspendCount(pushApplicationId, fromDate, toDate);
		}

		/// <summary>
		/// Gets a count of the number of users that unsubscribed from an application between the given date range.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="fromDate">From date.</param>
		/// <param name="toDate">To date.</param>
		/// <returns>A count of users who unsubscribed.</returns>
		public int GetUnsubscribeCount(string pushApplicationId, DateTime fromDate, DateTime toDate)
		{
			return Repository.GetUnsubscribeCount(pushApplicationId, fromDate, toDate);
		}

		/// <summary>
		/// Returns a list of subscribers for the given push application id that are in the specified subscriber id list with the specified status.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberIds">The subscriber ids.</param>
		/// <param name="status">The status.</param>
		/// <returns>
		/// An enumeration of <see cref="SubscriberPartial"/> corresponding to matching subscribers.
		/// </returns>
		public IEnumerable<SubscriberPartial> MatchByAppIdAndIdsAndStatus(string pushApplicationId, IEnumerable<string> subscriberIds, SubscriberStatus status)
		{
			return Repository.FindByAppIdAndIdsAndStatus(pushApplicationId, subscriberIds, status);
		}

		/// <summary>
		/// Notifies the PPG directly with a suspend/resume/unsubscribe request for a given address.
		/// </summary>
		/// <param name="subscriptionType">Type of the subscription.</param>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="address">The address.</param>
		/// <returns>
		/// A <see cref="Task"/> that can be used to determine when the asynchronous operation has been completed.
		/// </returns>
		public Task NotifyPPG(SubscriptionType subscriptionType, string pushApplicationId, string address)
		{
			PushApplication app = ApplicationService().FindByAppId(pushApplicationId);
			if (app == null)
			{
				throw new InvalidOperationException("Push application not found.");
			}
			if (app.Type == PushApplicationType.EnterprisePush)
			{
				throw new InvalidOperationException("The PPG does not support subscription operations.");
			}

			string ppgUriPrefix;
			switch (subscriptionType)
			{
				case SubscriptionType.Unsubscribe:
					ppgUriPrefix = PushProperties.SubscriptionDeregistrationUrl;
					break;

				case SubscriptionType.Suspend:
					ppgUriPrefix = PushProperties.SubscriptionSuspendUrl;
					break;

				case SubscriptionType.Resume:
					ppgUriPrefix = PushProperties.SubscriptionResumeUrl;
					break;

				default:
					throw new ArgumentException("Unknown subscription type specified.");
			}

			return NotifyPPGCore(ppgUriPrefix, app.RimApplicationId, app.PublicPassword, address);
		}

		/// <summary>
		/// Subscribes a user to a push application.
		/// </summary>
		/// <param name="subscribeRequest">The subscribe request.</param>
		/// <returns>
		/// A <see cref="Task"/> that can be used to determine when the asynchronous operation has been completed.
		/// </returns>
		public async Task Subscribe(SubscribeRequest subscribeRequest)
		{
			List<SwapBISUnsubscribeRequest> swapUnsubscribeList =
				new List<SwapBISUnsubscribeRequest>();
			List<UnsubscribeRequest> swapNotificationList =
				new List<UnsubscribeRequest>();
			bool shouldResubscribe;
			try
			{
				shouldResubscribe = ValidateSubscribe(subscribeRequest, swapUnsubscribeList, swapNotificationList);
			}
			catch (Exception e)
			{
				ContentProviderSubscriptionService.OnSubscribeFailure(subscribeRequest, e);
				throw e;
			}

			PushApplication pushApp = ApplicationService().FindByAppId(subscribeRequest.PushApplicationId);
			try
			{
				if (shouldResubscribe)
				{
					Repository.Resubscribe(subscribeRequest);
				}
				else
				{
					var subList = Repository.FindByIdAndNotStatus(
						subscribeRequest.SubscriberId, SubscriberStatus.Inactive);

					if (!subList.Any())
					{
						SubscriberSwap(subscribeRequest, swapUnsubscribeList, swapNotificationList);
					}
					else
					{
						string oldAddress = GetOldAddress(subList);

						if (!oldAddress.Equals(subscribeRequest.Address, StringComparison.OrdinalIgnoreCase))
						{
							DeviceSwap(oldAddress, subscribeRequest.SubscriberId, subscribeRequest.PushApplicationId, swapUnsubscribeList, swapNotificationList);
							SubscriberSwap(subscribeRequest, swapUnsubscribeList, swapNotificationList);
						}
					}

					Repository.Subscribe(subscribeRequest);
				}
			}
			catch (Exception e)
			{
				//log.error("DataAccessException caught: ", e);
				ContentProviderSubscriptionService.OnSubscribeFailure(subscribeRequest, e);
				throw e;
			}

			ContentProviderSubscriptionService.OnSubscribeSuccess(subscribeRequest);

			foreach (UnsubscribeRequest unsubRequest in swapNotificationList)
			{
				ContentProviderSubscriptionService.OnAutomaticUnsubscribe(unsubRequest);
			}

			string subDeregistrationUrl;
			if (swapUnsubscribeList.Any())
			{
				subDeregistrationUrl = PushProperties.SubscriptionDeregistrationUrl;

				foreach (SwapBISUnsubscribeRequest swapUnsubRequest in swapUnsubscribeList)
				{
					for (int attempt = 0; attempt < PushProperties.SubscriptionNotifyMaxRetries; ++attempt)
					{
						try
						{
							await NotifyPPGCore(subDeregistrationUrl, swapUnsubRequest.PushApplicationId, swapUnsubRequest.PushApplicationPassword, swapUnsubRequest.SubscriberAddress);
							break;
						}
						catch (Exception e)
						{
							//log.warn("PPGCommunicationFailureException caught - retrying: ", e);
						}
					}
				}
			}
		}

		/// <summary>
		/// Suspends a user's subscription to a push application.
		/// </summary>
		/// <param name="suspendRequest">The suspend request.</param>
		/// <returns>
		/// A <see cref="Task"/> that can be used to determine when the asynchronous operation has been completed.
		/// </returns>
		public async Task SuspendSubscription(SuspendRequest suspendRequest)
		{
			Subscriber subscriber;
			try
			{
				subscriber = ValidateSuspendSubscription(suspendRequest);
			}
			catch (Exception e)
			{
				ContentProviderSubscriptionService.OnSuspendFailure(suspendRequest, e);
				throw e;
			}

			try
			{
				Repository.Suspend(suspendRequest);
			}
			catch (Exception e)
			{
				//log.error("DataAccessException caught: ", e);
				ContentProviderSubscriptionService.OnSuspendFailure(suspendRequest, e);
				throw e;
			}

			ContentProviderSubscriptionService.OnSuspendSuccess(suspendRequest);

			if (subscriber.Type == SubscriberType.Public)
			{
				PushApplication app = ApplicationService().FindByAppId(subscriber.PushApplicationId);
				string subSuspendUrl = PushProperties.SubscriptionSuspendUrl;

				for (int attempt = 0; attempt < PushProperties.SubscriptionNotifyMaxRetries; ++attempt)
				{
					try
					{
						await NotifyPPGCore(subSuspendUrl, app.RimApplicationId, app.PublicPassword, subscriber.Address);
						break;
					}
					catch (Exception e)
					{
						//log.warn("PPGCommunicationFailureException caught - retrying once: ", e);
					}
				}
			}
		}

		/// <summary>
		/// Resumes a user's subscription to a push application.
		/// </summary>
		/// <param name="resumeRequest">The resume request.</param>
		/// <returns>
		/// A <see cref="Task"/> that can be used to determine when the asynchronous operation has been completed.
		/// </returns>
		public async Task ResumeSubscription(ResumeRequest resumeRequest)
		{
			Subscriber subscriber;
			try
			{
				subscriber = ValidateResumeSubscription(resumeRequest);
			}
			catch (Exception e)
			{
				ContentProviderSubscriptionService.OnResumeFailure(resumeRequest, e);
				throw e;
			}

			try
			{
				Repository.Resume(resumeRequest);
			}
			catch (Exception e)
			{
				//log.error("DataAccessException caught: ", e);
				ContentProviderSubscriptionService.OnResumeFailure(resumeRequest, e);
				throw e;
			}

			ContentProviderSubscriptionService.OnResumeSuccess(resumeRequest);

			if (subscriber.Type == SubscriberType.Public)
			{
				PushApplication app = ApplicationService().FindByAppId(subscriber.PushApplicationId);
				string subResumeUrl = PushProperties.SubscriptionResumeUrl;

				for (int attempt = 0; attempt < PushProperties.SubscriptionNotifyMaxRetries; ++attempt)
				{
					try
					{
						await NotifyPPGCore(subResumeUrl, app.RimApplicationId, app.PublicPassword, subscriber.Address);
						break;
					}
					catch (Exception e)
					{
						//log.warn("PPGCommunicationFailureException caught - retrying once: ", e);
					}
				}
			}
		}

		/// <summary>
		/// Unsubscribes a user from a push application.
		/// </summary>
		/// <param name="unsubscribeRequest">The unsubscribe request.</param>
		/// <returns>
		/// A <see cref="Task"/> that can be used to determine when the asynchronous operation has been completed.
		/// </returns>
		public async Task Unsubscribe(UnsubscribeRequest unsubscribeRequest)
		{
			Subscriber subscriber;
			try
			{
				subscriber = ValidateUnsubscribe(unsubscribeRequest);
			}
			catch (Exception e)
			{
				ContentProviderSubscriptionService.OnUnsubscribeFailure(unsubscribeRequest, e);
				throw e;
			}

			try
			{
				Repository.Unsubscribe(unsubscribeRequest);
			}
			catch (Exception e)
			{
				//log.error("DataAccessException caught: ", e);
				ContentProviderSubscriptionService.OnUnsubscribeFailure(unsubscribeRequest, e);
				throw e;
			}

			if (unsubscribeRequest.IsSystemGenerated)
			{
				ContentProviderSubscriptionService.OnAutomaticUnsubscribe(unsubscribeRequest);
			}
			else
			{
				ContentProviderSubscriptionService.OnUnsubscribeSuccess(unsubscribeRequest);
			}

			if (subscriber.Type == SubscriberType.Public && unsubscribeRequest.NotifyGateway)
			{
				PushApplication app = ApplicationService().FindByAppId(subscriber.PushApplicationId);
				string subDeregistrationUrl = PushProperties.SubscriptionDeregistrationUrl;

				for (int attempt = 0; attempt < PushProperties.SubscriptionNotifyMaxRetries; ++attempt)
				{
					try
					{
						await NotifyPPGCore(subDeregistrationUrl, app.RimApplicationId, app.PublicPassword, subscriber.Address);
						break;
					}
					catch (Exception e)
					{
						//log.warn("PPGCommunicationFailureException caught - retrying once: ", e);
					}
				}
			}
		}

		/// <summary>
		/// Syncs the status of subscribers in storage by looking up subscribers on the PPG end that have the given status and updates the corresponding subscribers on the SDK side.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="statusInPPG">The status in PPG.</param>
		/// <param name="syncedBy">The synced by.</param>
		/// <returns>
		/// A <see cref="Task"/> that when complete will return the sync status in a <see cref="SubscriberSyncResult"/> object.
		/// </returns>
		public async Task<SubscriberSyncResult> SyncSubscribersByStatusInPPG(
			string pushApplicationId, SubscriberStatus statusInPPG, string syncedBy)
		{
			//log.info("Syncing subscriber statuses with the PPG for push applicationId=" + pushApplicationId + ", syncedBy=" + syncedBy + ", statusInPPG=" + statusInPPG);
			if (string.IsNullOrEmpty(pushApplicationId))
			{
				throw new ArgumentNullException("pushApplicationId");
			}
			if (string.IsNullOrEmpty(syncedBy))
			{
				throw new ArgumentNullException("syncedBy");
			}
			if (statusInPPG == SubscriberStatus.Inactive)
			{
				throw new ArgumentException("PPG subscriber status can only be ACTIVE or SUSPENDED.");
			}

			PushApplication pushApplication = ValidatePushApplication(pushApplicationId);
			if (pushApplication.Type == PushApplicationType.EnterprisePush)
			{
				throw new NotSupportedException("The push application " + pushApplicationId + " has a type of " + PushApplicationType.EnterprisePush + ".  The Enterprise PPG does not support a subscription sync operation.");
			}

			SubscriptionQueryResponse subQueryResponse = null;
			try
			{
				SubscriptionQueryControl control =
					new SubscriptionQueryControl
					{
						IsUsingPublicPush = true,
						PushProperties = PushProperties,
						Status = (SubscriptionStatus)statusInPPG
					};
				subQueryResponse = await PapService.SubscriptionQuery(
					pushApplicationId, pushApplication.PublicPassword, control);
			}
			catch (Exception e)
			{
				//log.error("UnauthorizedException caught: ", e);
				throw e;
			}

			SubscriberSyncResult syncResult = new SubscriberSyncResult();

			OrderablePartitioner<SubscriptionQueryResult> part =
				ChunkPartitioner.Create<SubscriptionQueryResult>(
					subQueryResponse.Results,
					PushProperties.SubscriptionPpgSyncBatchSize);
			Parallel.ForEach(
				part,
				new ParallelOptions
				{
					MaxDegreeOfParallelism = PushProperties.SubscriptionPpgSyncMaxThreads
				},
				(entry) =>
				{
					var subscribers = FindByAppIdAndAddress(pushApplicationId, entry.Address);
					SubscriptionType subscriptionType = SubscriptionType.Unsubscribe;
					string subscriberId = null;
					try
					{
						bool hasActiveOrSuspendedSubscriber = false;
						foreach (SubscriberPartial sub in subscribers)
						{
							if (sub.Status == SubscriberStatus.Active)
							{
								if (entry.Status == SubscriptionStatus.Suspended)
								{
									subscriptionType = SubscriptionType.Suspend;
									subscriberId = sub.SubscriberId;
									SuspendSubscription(new SuspendRequest(pushApplicationId, sub.SubscriberId));
								}
								hasActiveOrSuspendedSubscriber = true;
								break;
							}
							if (sub.Status == SubscriberStatus.Suspended)
							{
								if (entry.Status == SubscriptionStatus.Active)
								{
									subscriptionType = SubscriptionType.Suspend;
									NotifyPPG(SubscriptionType.Suspend, pushApplicationId, entry.Address);
								}
								hasActiveOrSuspendedSubscriber = true;
								break;
							}
						}

						if (subscribers.Any() && !hasActiveOrSuspendedSubscriber)
						{
							subscriptionType = SubscriptionType.Unsubscribe;
							NotifyPPG(SubscriptionType.Unsubscribe, pushApplicationId, entry.Address);
						}
					}
					catch (WebException e)
					{
						syncResult.PpgSubscriberUpdateFailures.Add(
							new PPGSubscriberUpdateFailure(
								subscriptionType, pushApplicationId, pushApplication.PublicPassword, entry.Address));
						//log.error("PPGCommunicationFailureException caught for appId=" + this.pushAppId + ", address=" + result.getAddress() + ", subType=" + subscriptionType + ": ", e);
					}
					catch (Exception e)
					{
						syncResult.SdkSubscriberUpdateFailures.Add(
							new SDKSubscriberUpdateFailure(subscriptionType, pushApplicationId, subscriberId));
						//log.error("InvalidPushAppException caught for appId=" + this.pushAppId + ", subId=" + subscriberId + ", subType=" + subscriptionType + ": ", e);
					}
				});
			return syncResult;
		}

		/// <summary>
		/// Syncs the status of subscribers in storage with the status on the PPG end.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="status">The status.</param>
		/// <param name="startIndex">The start index.</param>
		/// <param name="endIndex">The end index.</param>
		/// <param name="syncedBy">The synced by.</param>
		/// <returns>
		/// A <see cref="Task"/> that when complete will return the sync status in a <see cref="SubscriberSyncResult"/> object.
		/// </returns>
		public async Task<SubscriberSyncResult> SyncSubscribersWithPPGForStatus(string pushApplicationId, SubscriberStatus status, int startIndex, int endIndex, string syncedBy)
		{
			PushApplication app = ApplicationService().FindByAppId(pushApplicationId);
			if (app == null)
			{
				throw new ArgumentException("Application not found.");
			}
			if (app.Type == PushApplicationType.EnterprisePush)
			{
				throw new ArgumentException("Subscription not supported for Enterprise Push application.");
			}

			SubscriberSyncResult result = new SubscriberSyncResult();
			int totalEntities = endIndex - startIndex + 1;
			int batchSize = PushProperties.SubscriptionPpgSyncBatchSize;
			if (totalEntities < batchSize)
			{
				await SyncSubscribersWithPPGForStatusAsync(
					result, app, status, startIndex, endIndex);
			}
			else
			{
				// Determine number of batches
				int batchCount = totalEntities / batchSize;
				if ((totalEntities % batchSize) != 0)
				{
					++batchCount;
				}

				// Build list of start and end tuples
				List<Tuple<int, int>> blocks = new List<Tuple<int, int>>();
				for (int index = 0; index < batchCount; ++index)
				{
					int start = index * batchCount;
					int end = Math.Min(start + batchCount - 1, endIndex);
					blocks.Add(new Tuple<int, int>(start, end));
				}

				SemaphoreSlim maxThreads = new SemaphoreSlim(
					PushProperties.SubscriptionPpgSyncMaxThreads,
					PushProperties.SubscriptionPpgSyncMaxThreads);
				await TaskEx.WhenAll(
					(from item in blocks
					 select Task.Factory.StartNew(() =>
						 SyncSubscribersWithPPGForStatusAsync(
						 result, app, status, item.Item1, item.Item2, maxThreads))).ToArray());
			}

			return result;
		}

		/// <summary>
		/// Syncs the status of a single subscriber in storage with its status on the PPG end.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberId">The subscriber id.</param>
		/// <param name="syncedBy">The synced by.</param>
		/// <returns>
		/// A <see cref="Task"/> that when complete will return the full subscriber info in a <see cref="Subscriber"/> object.
		/// </returns>
		public async Task<Subscriber> SyncSubscriberWithPPG(string pushApplicationId, string subscriberId, string syncedBy)
		{
			//log.info("Syncing subscriber with the PPG for pushApplicationId=" + pushApplicationId + ", syncedBy=" + syncedBy + ", subscriberId=" + subscriberId);
			if (string.IsNullOrEmpty(pushApplicationId))
			{
				throw new ArgumentNullException("pushApplicationId");
			}
			if (string.IsNullOrEmpty(subscriberId))
			{
				throw new ArgumentNullException("subscriberId");
			}
			if (string.IsNullOrEmpty(syncedBy))
			{
				throw new ArgumentNullException("syncedBy");
			}

			PushApplication pushApplication = ValidatePushApplication(pushApplicationId);

			if (pushApplication.Type == PushApplicationType.EnterprisePush)
			{
				throw new NotSupportedException("The push application " + pushApplicationId + " has a type of " + PushApplicationType.EnterprisePush + ".  The Enterprise PPG does not support a subscription sync operation.");
			}

			Subscriber subscriber = ValidateSubscriberExists(pushApplicationId, subscriberId);

			if (subscriber.Type == SubscriberType.Enterprise)
			{
				throw new NotSupportedException("The subsciber " + subscriberId + "has a type of " + SubscriberType.Enterprise + ".  The Enterprise PPG does not support a subscription sync operation.");
			}

			if (subscriber.Status == SubscriberStatus.Inactive)
			{
				bool hasActiveOrSuspendedSubscriber = false;
				var subList = FindByAppIdAndAddress(pushApplicationId, subscriber.Address);

				foreach (SubscriberPartial sub in subList)
				{
					if (sub.Status == SubscriberStatus.Active || sub.Status == SubscriberStatus.Suspended)
					{
						hasActiveOrSuspendedSubscriber = true;
						break;
					}
				}

				if (hasActiveOrSuspendedSubscriber)
				{
					//log.info("No sync was performed as there is an active or suspended subscriber using the same address for the push application (pushAppId=" + pushApplicationId + ", subId=" + subscriberId + ", address=" + subscriber.getAddress() + ").");
					return subscriber;
				}
			}

			try
			{
				SubscriptionQueryControl control =
					new SubscriptionQueryControl
					{
						IsUsingPublicPush = true,
						PushProperties = PushProperties,
						Addresses =
							new string[]
							{
								subscriber.Address,
							}
					};
				SubscriptionQueryResponse subQueryResponse =
					await PapService.SubscriptionQuery(
						pushApplicationId, 
						pushApplication.PublicPassword,
						control);
				if (subQueryResponse.Results.Length != 1)
				{
					throw new InvalidOperationException("Expected only 1 status result back from the PPG but received " + subQueryResponse.Results.Length + ".");
				}

				SubscriptionQueryResult result = (SubscriptionQueryResult)subQueryResponse.Results[0];
				if (result.Status == SubscriptionStatus.Unknown)
				{
					//log.warn("The address " + result.Address + " could not be found on the PPG during a subscriber sync for the push application " + pushApplicationId + ".  Subscriber with id " + subscriberId + " will be unsubscribed from the SDK.  Deletion of the subscriber is recommended " + "using the deleteInactiveSubscription method.");
					if (subscriber.Status == SubscriberStatus.Active ||
						subscriber.Status == SubscriberStatus.Suspended)
					{
						await Unsubscribe(new UnsubscribeRequest(pushApplicationId, subscriberId));
					}
				}
				else if (subscriber.Status == SubscriberStatus.Active)
				{
					if (result.Status == SubscriptionStatus.Suspended)
					{
						await SuspendSubscription(new SuspendRequest(pushApplicationId, subscriberId));
					}
					else if (result.Status == SubscriptionStatus.Unsubscribed)
					{
						await Unsubscribe(new UnsubscribeRequest(pushApplicationId, subscriberId));
					}
				}
				else if (subscriber.Status == SubscriberStatus.Suspended)
				{
					if (result.Status == SubscriptionStatus.Active)
					{
						await NotifyPPG(SubscriptionType.Suspend, pushApplicationId, result.Address);
					}
					else if (result.Status == SubscriptionStatus.Unsubscribed)
					{
						await Unsubscribe(new UnsubscribeRequest(pushApplicationId, subscriberId));
					}
				}
				else if (result.Status == SubscriptionStatus.Active ||
					result.Status == SubscriptionStatus.Suspended)
				{
					await NotifyPPG(SubscriptionType.Unsubscribe, pushApplicationId, result.Address);
				}
			}
			catch (Exception e)
			{
				//log.error("UnauthorizedException caught: ", e);
				throw e;
			}

			return FindByAppIdAndId(subscriber.PushApplicationId, subscriber.SubscriberId);
		}

		/// <summary>
		/// Validates the passed in list of subscriber ids to determine which ones do not exist, which ones are active, which ones are inactive, and which ones are suspended.
		/// </summary>
		/// <param name="pushApplicationId">The push application id.</param>
		/// <param name="subscriberIds">The subscriber ids.</param>
		/// <returns>
		/// A <see cref="SubscriptionValidationResult"/> containing the results of the validation.
		/// </returns>
		public SubscriptionValidationResult ValidateSubscriptions(string pushApplicationId, IEnumerable<string> subscriberIds)
		{
			SubscriptionValidationResult result = new SubscriptionValidationResult();

			// TODO: Add support for partitioning this using TPL if the list
			//	of subscribers is more than the high water mark
			foreach (string subscriberId in subscriberIds)
			{
				Subscriber sub = Repository.FindByAppIdAndId(pushApplicationId, subscriberId);
				if (sub == null)
				{
					result.DoNotExistSubscriberIds.Add(subscriberId);
				}
				else if (sub.Status == SubscriberStatus.Active)
				{
					result.ActiveAddresses.Add(sub.Address);
					result.ActiveSubscribers.Add(sub);
				}
				else if (sub.Status == SubscriberStatus.Inactive)
				{
					result.InactiveSubscriberIds.Add(subscriberId);
				}
				else if (sub.Status == SubscriberStatus.Suspended)
				{
					result.SuspendedSubscriberIds.Add(subscriberId);
				}
			}
			return result;
		}

		#endregion
	}
}
